using System;
using System.Linq;
using System.Linq.Expressions;
using NUnit.Framework;
using System.Data;
using PropertyExpression.Common;
using PropertyExpression.ReactiveRelation.Supplementary;
using PropertyExpression.ReactiveRelation.Test.Common;
using PropertyExpression.ReactiveRelation.Values;

namespace PropertyExpression.ReactiveRelation.Test
{
    [TestFixture]
    public class QuoteTest : BaseDbTest
    {
        [Test]
        public void CalcTotalTest()
        {
            InitAndCalcTotal(EmptyRateFormulaAction, SetRate, GetCalcTotals(), delegate {  });
        }

        [Test]
        public void ConcurrentThreadsCalcTotalTest()
        {
            var contextProvider = InitCalc(EmptyRateFormulaAction);

            ConcurrentThreadsExecuter.Execute(
                () => CalcTotal(contextProvider, SetRate, GetCalcTotals(), IsolationLevel.ReadCommitted, delegate { })
                );
        }

        [Test]
        public void SubstitutedFormulasCalcTotalTest()
        {
            ProcessQuote(
                new ContextProvider(
                    registrator =>
                    registrator.Register<Quote>(
                        binder =>
                        binder._(_ => _.QuoteTotal)(
                            it => it._(_ => _.QuoteJobs)
                                      .Sum(
                                      job => job.LaborSubtotal().Add(job.PartSubtotal())
                                      )
                                      .Add(
                                      it._(_ => _.QuoteOtherExpenses)
                                          .Sum(
                                          expense => expense._(_ => _.Quantity)
                                                          .Mult(
                                                          expense._(_ => _.Quote)._(_ => _.QuoteJobs).Sum(
                                                              job => job.LaborSubtotal()
                                                              )
                                                              .Percent(
                                                              expense._(_ => _.LaborPercent)
                                                              )
                                                              .Add(
                                                              expense._(_ => _.Quote)._(_ => _.QuoteJobs)
                                                                  .Sum(
                                                                  job => job.PartSubtotal()
                                                                  )
                                                                  .Percent(
                                                                  expense._(_ => _.PartPercent))
                                                              )
                                                              .Add(
                                                              expense._(_ => _.Fix)
                                                              )
                                                          )
                                          )
                                      )
                            )
                        )
                    ),
                delegate
                    {
                        //no-op
                    },
                SetRate, GetCalcTotals(), IsolationLevel.Serializable, delegate { });
        }

        [Test]
        public void IfAndNotCalcCalcTotalTest()
        {
            IfCalcTotalTest(it => it.NotCalc());
        }

        [Test]
        public void IfAndGetStoredValueCalcTotalTest()
        {
            IfCalcTotalTest(it => it.GetStoredValue());
        }

        [Test]
        public void CacheFuncTest()
        {
            new ContextProvider(
                registrator =>
                registrator.Register<Quote>(
                    binder =>
                        {
                            var copier = new DealerQuoteCopier(binder);
                            copier._(_ => _.DealerName)(_ => _.DealerCompanyName);
                            copier._(_ => _.DealerPhone)(_ => _.DealerPhone);
                            copier._(_ => _.DealerFax)(_ => _.DealerFax);
                        }
                    ))
                .Execute(
                (context, calcExecutor) =>
                    {
                        var quote = calcExecutor.CreateAndCoupleEntity<Quote>();
                        context.Quotes.InsertOnSubmit(quote.Entity);

                        var login = BusinessFacade.GetDealer(context);

                        quote._(_ => _.DealerId)(login.DealerId);

                        Assert.AreEqual(login.DealerCompanyName, quote.Entity.DealerName);
                        Assert.AreEqual(login.DealerPhone, quote.Entity.DealerPhone);
                        Assert.AreEqual(login.DealerFax, quote.Entity.DealerFax);
                    },
                IsolationLevel.Serializable);
        }

        [Test]
        public void BatchCalcTotalTest()
        {
            InitCalc(EmptyRateFormulaAction)
                .Execute(
                (context, calcExecutor) =>
                    {
                        ICalcInitiator<Quote> quote = null;
                        calcExecutor.CalcBatch(
                            () =>
                                {
                                    quote = BusinessFacade.CreateQuote(context, calcExecutor,
                                                                       () => BusinessFacade.GetDealer(context));

                                    Func<decimal, decimal, ICalcInitiator<QuoteJob>> createQuoteJobFunc =
                                        (jobTime, rate) =>
                                            {
                                                var job = calcExecutor.CreateAndCoupleEntity<QuoteJob>();
                                                job._(_ => _.QuoteJobId)(IdGenerator.GetId());

                                                quote.Entity.AddChild(_ => _.QuoteJobs, job);

                                                job._(_ => _.LaborTime)(jobTime);
                                                job._(_ => _.LaborRate)(rate);

                                                return job;
                                            };

                                    var quoteJob = createQuoteJobFunc(2.35m, 15);

                                    Func<int, decimal, ICalcInitiator<QuotePart>> createQuotePartFunc =
                                        (quantity, price) =>
                                            {
                                                var part = calcExecutor.CreateAndCoupleEntity<QuotePart>();
                                                part._(_ => _.QuotePartId)(IdGenerator.GetId());

                                                quoteJob.Entity.AddChild(_ => _.QuoteParts, part);

                                                part._(_ => _.Quantity)(quantity);
                                                part._(_ => _.Price)(price);

                                                return part;
                                            };

                                    var quotePart = createQuotePartFunc(2, 15.22m);

                                    createQuotePartFunc(3, 33.72m);

                                    quotePart._(_ => _.Price)(203.44m);
                                }
                            );
                        Assert.AreEqual(1046.04m, quote.Entity.QuoteTotal);
                    }, IsolationLevel.Serializable);
        }

        [Test]
        [ExpectedException(typeof(InvalidOperationException))]
        public void SetFKIdTest()
        {
            InitAndCalcTotal(
                EmptyRateFormulaAction, SetRate, GetCalcTotals(),
                (quotePartId, quoteJobId, isolationLevel, contextProvider) =>
                contextProvider.Execute(
                    (context, calcExecutor) =>
                        {
                            var quoteLine =
                                context.QuoteParts.Single(_ => _.QuotePartId == quotePartId).Couple
                                    (calcExecutor);

                            quoteLine._(_ => _.QuoteJobId)(quoteJobId);
                        },
                    isolationLevel
                    )
                );
        }

        private static void ProcessQuote(
            ContextProvider contextProvider, 
            Action<Action<decimal>, Guid> changeOtherExpensesPriceAction, 
            Action<ICalcInitiator<QuoteJob>, decimal> setRateAction, 
            Totals totals, 
            IsolationLevel isolationLevel,
            Action<Guid, Guid, IsolationLevel, ContextProvider> setFKIdAction)
        {
            Func<Guid> createQuoteFunc =
                () => contextProvider.Execute(
                          (context, calcExecutor) =>
                              {
                                  var id =
                                      BusinessFacade.CreateQuote(context, calcExecutor,
                                                                 () => BusinessFacade.GetDealer(context))
                                          .Entity.QuoteId;

                                  context.SubmitChanges();

                                  return id;
                              },
                          isolationLevel
                          );

            var quoteId = createQuoteFunc();

            Func<decimal, decimal, Guid> createQuoteJobFunc =
                (jobTime, rate) =>
                contextProvider.Execute(
                    (context, calcExecutor) =>
                        {
                            var quoteJob = calcExecutor.CreateAndCoupleEntity<QuoteJob>();

                            quoteJob._(_ => _.QuoteJobId)(IdGenerator.GetId());

                            quoteJob._(_ => _.Quote)(context.Quotes.Single(_ => _.QuoteId == quoteId));

                            quoteJob._(_ => _.LaborTime)(jobTime);
                            setRateAction(quoteJob, rate);

                            var jobId = quoteJob.Entity.QuoteJobId;

                            context.SubmitChanges();

                            return jobId;
                        }, isolationLevel);

            var quoteJobId = createQuoteJobFunc(2.35m, 15);

            Func<int, decimal, Guid> createQuotePartFunc =
                (quantity, price) =>
                contextProvider.Execute(
                    (context, calcExecutor) =>
                        {
                            var quotePart = calcExecutor.CreateAndCoupleEntity<QuotePart>();
                            quotePart._(_ => _.QuotePartId)(IdGenerator.GetId());

                            quotePart._(_ => _.QuoteJob)(context.QuoteJobs.Single(_ => _.QuoteJobId == quoteJobId));

                            quotePart._(_ => _.Quantity)(quantity);
                            quotePart._(_ => _.Price)(price);

                            var partId = quotePart.Entity.QuotePartId;

                            context.SubmitChanges();

                            return partId;
                        }, isolationLevel);

            var quotePartId = createQuotePartFunc(2, 15.22m);

            Action<Guid, decimal> verifyTotalByQuoteAction =
                (localQuoteId, expected) =>
                contextProvider.Execute(
                    (context, calcExecutor) =>
                    Assert.AreEqual(expected, context.Quotes.Single(_ => _.QuoteId == localQuoteId).QuoteTotal),
                    isolationLevel
                    );

            Action<decimal> verifyTotalAction =
                expected => verifyTotalByQuoteAction(quoteId, expected);

            verifyTotalAction(totals.Total1);

            createQuotePartFunc(3, 33.72m);

            verifyTotalAction(totals.Total2);

            contextProvider.Execute(
                (context, calcExecutor) => 
                    {
                        context.QuoteParts.Single(_ => _.QuotePartId == quotePartId)
                            .Couple(calcExecutor)._(_ => _.Price)(203.44m);

                        context.SubmitChanges();
                    }, isolationLevel);

            verifyTotalAction(totals.Total3);

            contextProvider.Execute(
                (context, calcExecutor) => 
                    {
                        var quoteLine = context.QuoteParts.Single(_ => _.QuotePartId == quotePartId)
                            .Couple(calcExecutor);
                        quoteLine._(_ => _.QuoteJob)(default(QuoteJob));
                        context.QuoteParts.DeleteOnSubmit(quoteLine.Entity);

                        context.SubmitChanges();
                    }, isolationLevel);

            verifyTotalAction(totals.Total4);

            var quoteJobId1 = quoteJobId;
            quoteJobId = createQuoteJobFunc(758.19m, 124m);

            verifyTotalAction(totals.Total5);

            var quoteLineId2 = createQuotePartFunc(23, 1.22m);
            createQuotePartFunc(7, 15.22m);

            setFKIdAction(quoteLineId2, quoteJobId1, isolationLevel, contextProvider);

            verifyTotalAction(totals.Total6);

            var quoteId2 = createQuoteFunc();

            Action<Guid> setQuoteAction =
                localQuoteId =>
                contextProvider.Execute(
                    (context, calcExecutor) =>
                        {
                            var quoteJob = context.QuoteJobs.Single(job => job.QuoteJobId == quoteJobId1)
                                .Couple(calcExecutor);

                            quoteJob._(_ => _.Quote)(context.Quotes.Single(quote => quote.QuoteId == localQuoteId));

                            context.SubmitChanges();
                        }, isolationLevel);

            setQuoteAction(quoteId2);

            verifyTotalAction(totals.Total11);
            verifyTotalByQuoteAction(quoteId2, totals.Total12);

            setQuoteAction(quoteId);

            verifyTotalAction(totals.Total6);
            verifyTotalByQuoteAction(quoteId2, 375m);

            contextProvider.Execute(
                (context, calcExecutor) =>
                    {
                        var otherExpense = (from expense in context.QuoteOtherExpenses
                                            where expense.PartPercent == 18 && expense.QuoteId == quoteId
                                            select expense).Single().Couple(calcExecutor);
                        otherExpense._(_ => _.Quote)(default(Quote));
                        context.QuoteOtherExpenses.DeleteOnSubmit(otherExpense.Entity);

                        context.SubmitChanges();
                    }, isolationLevel);

            verifyTotalAction(totals.Total7);

            contextProvider.Execute(
                (context, calcExecutor) => 
                    {
                        var otherExpense = calcExecutor.CreateAndCoupleEntity<QuoteOtherExpense>();
                        otherExpense._(_ => _.QuoteOtherExpenseId)(IdGenerator.GetId());

                        otherExpense._(_ => _.Quantity)(2);
                        otherExpense._(_ => _.LaborPercent)(43);
                        otherExpense._(_ => _.PartPercent)(77);
                        otherExpense._(_ => _.Fix)(53.2m);

                        otherExpense._(_ => _.Quote)(context.Quotes.Single(_ => _.QuoteId == quoteId));

                        context.SubmitChanges();
                    }, isolationLevel);

            verifyTotalAction(totals.Total8);

            changeOtherExpensesPriceAction(verifyTotalAction, quoteId);

            contextProvider.Execute(
                (context, calcExecutor) => 
                    {
                        var quoteJob = context.QuoteJobs.Single(_ => _.QuoteJobId == quoteJobId).Couple(calcExecutor);

                        foreach (var quotePart in quoteJob.Entity.QuoteParts.ToList())
                        {
                            quotePart.Couple(calcExecutor)._(_ => _.QuoteJob)(default(QuoteJob));
                            context.QuoteParts.DeleteOnSubmit(quotePart);
                        }

                        quoteJob._(_ => _.Quote)(default(Quote));
                        context.QuoteJobs.DeleteOnSubmit(quoteJob.Entity);

                        context.SubmitChanges();
                    }, isolationLevel);

            verifyTotalAction(totals.Total10);
        }

        private static Totals GetCalcTotals()
        {
            return new Totals
                       {
                           Total1 = 453.81m,
                           Total2 = 579.26m,
                           Total3 = 1046.04m,
                           Total4 = 541.51m,
                           Total5 = 110069.63m,
                           Total6 = 110236.54m,
                           Total7 = 110194.1m,
                           Total8 = 191547.28m,
                           Total9 = 192367.58m,
                           Total10 = 815.8m,
                           Total11 = 110070.05m,
                           Total12 = 541.51m
                       };
        }

        private static void EmptyRateFormulaAction(IBinder<DataClassesDataContext, QuoteJob> binder)
        {
            //no-op
        }

        private static void InitAndCalcTotal(Action<IBinder<DataClassesDataContext, QuoteJob>> rateFormulaAction, 
            Action<ICalcInitiator<QuoteJob>, decimal> setRateAction,
            Totals totals, Action<Guid, Guid, IsolationLevel, ContextProvider> setFKIdAction)
        {
            CalcTotal(InitCalc(rateFormulaAction), setRateAction, totals, IsolationLevel.Serializable, setFKIdAction);
        }

        private static void CalcTotal(ContextProvider contextProvider, 
            Action<ICalcInitiator<QuoteJob>, decimal> setRateAction, 
            Totals totals, 
            IsolationLevel isolationLevel, 
            Action<Guid, Guid, IsolationLevel, ContextProvider> setFKIdAction)
        {
            ProcessQuote(
                contextProvider,
                (verifyTotalAction, quoteId) =>
                    {
                        contextProvider.Execute(
                            (context, calcExecutor) =>
                                {
                                    (from expense in context.QuoteOtherExpenses
                                     where expense.PartPercent == 1 && expense.QuoteId == quoteId
                                     select expense).Single().Couple(calcExecutor)
                                        ._(_ => _.Price)(5575.2m);

                                    context.SubmitChanges();
                                }, isolationLevel);

                        verifyTotalAction(totals.Total9);
                    },
                setRateAction, totals, isolationLevel, setFKIdAction);
        }

        private static ContextProvider InitCalc(Action<IBinder<DataClassesDataContext, QuoteJob>> rateFormulaAction)
        {
            return new ContextProvider(
                registrator =>
                    {
                        registrator.Register<QuotePart>(
                            binder =>
                            binder._(_ => _.PartTotal)(
                                it => it._(_ => _.Quantity)
                                          .Mult(it._(_ => _.Price))
                                )
                            );

                        registrator.Register<QuoteJob>(
                            binder =>
                                {
                                    rateFormulaAction(binder);

                                    binder._(_ => _.LaborSubtotal)(
                                        it => it._(_ => _.LaborTime)
                                                  .Mult(it._(_ => _.LaborRate))
                                        );

                                    binder._(_ => _.PartSubtotal)(
                                        it => it._(_ => _.QuoteParts)
                                                  .Sum(part => part._(_ => _.PartTotal))
                                        );

                                    binder._(_ => _.JobTotal)(
                                        it => it._(_ => _.LaborSubtotal)
                                                  .Add(it._(_ => _.PartSubtotal))
                                        );
                                }
                            );

                        registrator.Register<Quote>(
                            binder =>
                                {
                                    binder._(_ => _.PartSubtotal)(
                                        it => it._(_ => _.QuoteJobs)
                                                  .Sum(job => job._(_ => _.PartSubtotal))
                                        );

                                    binder._(_ => _.LaborSubtotal)(
                                        it => it._(_ => _.QuoteJobs)
                                                  .Sum(job => job._(_ => _.LaborSubtotal))
                                        );

                                    binder._(_ => _.OtherExpenseSubtotal)(
                                        it => it._(_ => _.QuoteOtherExpenses)
                                                  .Sum(expenses => expenses._(_ => _.OtherExpenseTotal))
                                        );

                                    binder._(_ => _.QuoteTotal)(
                                        it => it._(_ => _.QuoteJobs)
                                                  .Sum(job => job._(_ => _.JobTotal))
                                                  .Add(it._(_ => _.OtherExpenseSubtotal))
                                        );
                                }
                            );

                        registrator.Register<QuoteOtherExpense>(
                            binder =>
                                {
                                    binder._(_ => _.OtherExpenseTotal)(
                                        it => it._(_ => _.Quantity)
                                                  .Mult(it._(_ => _.Price))
                                        );

                                    binder._(_ => _.Price)(
                                        it => it._(_ => _.Quote)._(_ => _.LaborSubtotal)
                                                  .Percent(it._(_ => _.LaborPercent))
                                                  .Add(
                                                  it._(_ => _.Quote)._(_ => _.PartSubtotal)
                                                      .Percent(it._(_ => _.PartPercent))
                                                  )
                                                  .Add(
                                                  it._(_ => _.Fix)
                                                  )
                                        );
                                }
                            );
                    }
                );
        }

        private static void SetRate(ICalcInitiator<QuoteJob> quoteJob, decimal rate)
        {
            quoteJob._(_ => _.RateIsInputted)(true);
            quoteJob._(_ => _.LaborRate)(rate);
        }

        private static void IfCalcTotalTest(Func<IPropertyBindContext<DataClassesDataContext, QuoteJob, decimal>, IValue<decimal>> rateFunc)
        {
            InitAndCalcTotal(binder =>
                             binder._(_ => _.LaborRate)(
                                 it => it._(_ => _.RateIsInputted).If(
                                           rateFunc(it),
                                           133m.Const()
                                           )
                                 ),
                             (quoteJob, rate) =>
                             quoteJob._(_ => _.RateIsInputted)(false),
                             new Totals
                                 {
                                     Total1 = 776.87m,
                                     Total2 = 902.32m,
                                     Total3 = 1369.1m,
                                     Total4 = 864.57m,
                                     Total5 = 118342.31m,
                                     Total6 = 118509.22m,
                                     Total7 = 118466.78m,
                                     Total8 = 205926.82m,
                                     Total9 = 206392.07m,
                                     Total10 = 1377.34m,
                                     Total11 = 118019.66m,
                                     Total12 = 864.57m
                                 }, delegate {  });
        }

        private class DealerQuoteCopier
        {
            private readonly IBinder<DataClassesDataContext, Quote> binder;
            private readonly Func<FuncCache, DataClassesDataContext, Func<Guid, Dealer>> getDealerFunc;

            public DealerQuoteCopier(IBinder<DataClassesDataContext, Quote> binder)
            {
                this.binder = binder;

                getDealerFunc = this.binder.CacheFunc(
                    context => TypeInferencer.Func(
                                   (Guid dealerId) => context.Dealers.Single(x => x.DealerId == dealerId)
                                   )
                    );
            }

            public Action<Func<Dealer, TResult>> _<TResult>(
                Expression<Func<Quote, TResult>> propertyExpression)
            {
                return valueFunc =>
                       binder._(propertyExpression)(
                           it =>
                           it._(_ => _.DealerId).IsDefault()
                               .If(
                               it.Default(),
                               it._(_ => _.DealerId)
                                   .Func(
                                   dealerId => valueFunc(it.Execute(getDealerFunc)(dealerId))
                                   )
                               )
                           );
            }
        }

        private class Totals
        {
            public decimal Total1 { get; set; }
            public decimal Total2 { get; set; }
            public decimal Total3 { get; set; }
            public decimal Total4 { get; set; }
            public decimal Total6 { get; set; }
            public decimal Total7 { get; set; }
            public decimal Total8 { get; set; }
            public decimal Total9 { get; set; }
            public decimal Total10 { get; set; }
            public decimal Total5 { get; set; }
            public decimal Total11 { get; set; }
            public decimal Total12 { get; set; }
        }

        protected override void DbClean()
        {
            DataContextProviderHolder.Provider
                .Execute(context =>
                             {
                                 context.QuoteJobs.DeleteAll();
                                 context.QuoteOtherExpenses.DeleteAll();
                                 context.Quotes.DeleteAll();
                                 context.QuoteParts.DeleteAll();

                                 context.SubmitChanges();
                             },
                         IsolationLevel.Serializable);
        }
    }
}